Developing Managed Stored Procedures
Stored procedures are a
great starting point for getting into SQLCLR because they are easy to
implement. To do so, right-click your new project in VS’s Solution
Explorer and then select Add, Stored Procedure. Name your new class StoredProcedures.cs. A partial class of that name opens in the VS code window. Note that VS automatically adds the required reference to Microsoft.SqlServer.Server and its associated using statement. Microsoft.SqlServer.Server contains the SqlProcedure attribute required for turning ordinary methods into SQLCLR stored procedures.
Change your autogenerated method name from StoredProcedures to GetProductReviews. Next, if you’re not working in VS, you need to decorate this method with the SqlProcedure attribute.
Attributes and the Implementation Contract
If you’ve never used attributes,
you can think of them as metadata that tells callers, usually through
reflection, that the decorated element (known as the target)
meets some criterion. All the managed objects you create in this article require certain attributes to be applied; otherwise, they cannot
be used in a SQL Server context.
The classes you build must
also implement particular methods and/or method signatures to be built
and deployed successfully to SQL Server. This is known as fulfilling the implementation contract. For stored procedures, fulfilling this contract requires that your method be marked static. Its return type must be one of the following: void, Int32, Nullable<Int32>, or SqlInt32.
Its input parameters and their types are up to you, but keep in mind
that these must be convertible from a T-SQL context to a .NET context.
These are the only contract requirements to be filled for stored
procedures.
Note
It makes sense that these methods must be marked static because they are called by the CLR host via the class’s type object, rather than via an object instance (that is, AssemblyName.ClassName.StaticMethodName(Parameters)).
Object-oriented (OO) purists
might suggest that this way of creating managed SQL Server objects could
have been done in a more OO-friendly way if the contract to be filled
required overriding the methods of an abstract class or implementing
interfaces. The static requirement, however, currently makes this
impossible because static members are not inherited and cannot be used
to implement interface members.
The constructor for the SqlProcedure
attribute is overloaded to either take zero parameters or take one
parameter that is actually a list of named parameters. (Having a list of
named parameters in the attribute signature is common to most of the
attributes used in this article, although the choice of named parameter
pairs varies from attribute to attribute.)
For the SqlStoredProcedureAttribute, only one named parameter exists: Name. You use Name when you want to name the method one thing but have the name it generates for use in a T-SQL context to be another name.
The code in Listing 1
illustrates the use of a named parameter in this attribute and contains
a simple example of how to generate a set of rows using SQLCLR.
Listing 1. A Managed Stored Procedure That Generates a Set of Rows
[SqlProcedure(Name = "clr_GetProductManuals")] public static void GetProductManuals() { using (SqlConnection ContextConnection = new SqlConnection("context connection=true")) { SqlDataRecord record = new SqlDataRecord( new SqlMetaData[] { new SqlMetaData("ProductModelId", SqlDbType.Int), new SqlMetaData("Manual", SqlDbType.Xml) } );
SqlContext.Pipe.SendResultsStart(record);
using (SqlCommand Command = new SqlCommand()) { Command.CommandText = @"SELECT TOP 10 ProductModelId, Instructions FROM Production.ProductModel WHERE Instructions IS NOT NULL";
Command.Connection = ContextConnection; ContextConnection.Open();
using (SqlDataReader reader = Command.ExecuteReader()) { while (reader.Read()) { int ProductModelId = reader.GetInt32(0); SqlXml ManualXml = reader.GetSqlXml(1);
record.SetInt32(0, ProductModelId); record.SetSqlXml(1, ManualXml); SqlContext.Pipe.SendResultsRow(record); } }
SqlContext.Pipe.SendResultsEnd(); } } }
|
Although
this example does not require it, if your SQLCLR code needs to access
server resources (such as files), it would be necessary to change your
assembly’s permission set from the default of SAFE to EXTERNAL_ACCESS.
To do to this in VS, right-click your project in Solution Explorer and
select Properties. Then, on the left side of the window, select the
Database tab. (Note that the Database tab is the place where VS stores a
connection string matching your database reference. You can change that
here as well.) Under the Permission Level drop-down, change the value
from Safe to External and save the project. You can also type in the
name of the SQL Server login (under Assembly Owner), which will be
specified for the AUTHORIZATION parameter of CREATE ASSEMBLY during deployment by VS.
The idea behind the code in Listing 1 is that, given a set of rows from Production.ProductModel,
the procedure generates a result set of only those products that have a
reference manual. Let’s examine the new objects in this code.
The Context Connection
In our managed procedure, we use a special ADO.NET connection string ("context connection=true”),
known as the context connection string, to connect to the session of
SQL Server under which our managed stored procedure is currently
executing (that is, once the assembly has been deployed and is running
in a T-SQL context).
Objects in Microsoft.SqlServer.Server
Our managed procedure also uses some specialized objects to send data to SQL Server through the active connection:
SqlContext—
This represents the server execution context for the managed routine.
You can think of it as the line of communication between the .NET and
SQL Server environments.
SqlContext.Pipe— SqlContext holds the crucial Pipe property, used to send SqlDataRecord
objects or text messages to the method’s caller, which, by the way, may
be either another managed routine (via ADO.NET) or any T-SQL user code.
SqlDataRecord— This is an abstraction that represents a record in any given table. The schema of columns for a SqlDataRecord object is created by using SqlMetaData objects, as shown in Listing 46.1.
SqlMetaData— An array of SqlMetaData objects is passed to the constructor of each SqlDataRecord. Each SqlMetaData object defines the name, type, precision, scale, and so forth for its target column via its overloaded constructors.
Returning to the code in Listing 46.1, before looping through our SqlDataReader object (reader), we call Pipe.SendResultsStart, passing a SqlDataRecord
object whose structure matches our desired output. This tells SQL
Server that our procedure is about to send rows (to the caller) having a
specific structure.
Looping through the reader (using while (reader.Read())), we select the values to be returned. To do this, we use the Set[DataTypeName] methods on our SqlDataRecord object called record. When our values are all set, we call SqlContext.Pipe.SendResultsRow(record) to return these data.
After the code has finished sending data to the client, it cleans up by calling Pipe.SendResultsEnd. Note that the Pipe object also has an ExecuteAndSend method that takes a SqlCommand
parameter, executes it, and sends all the results back to the caller in
one fell swoop. In addition, you can query the status of the Pipe object by checking its IsSendingResults Boolean property. You can even send an informational text message (similar to T-SQL’s print function) to the caller, using Pipe.Send("Text"). Send() is overloaded to accept a SqlDataRecord object or a SqlDataReader object that contains the data to be returned.
Building and Deploying the Assembly
At this point, you can build
the VS project and then choose the new Deploy command from VS’s Build
menu. In this step, VS generates the T-SQL DDL scripts needed to upload
our assembly to SQL Server and then add our managed stored procedure to
the AdventureWorks2008 database.
You’ve already seen the CREATE ASSEMBLY
DDL that VS uses. For now, let’s assume that you’ve already uploaded
the assembly once. In this scenario, you need to execute the following
T-SQL to replace that assembly with a newly compiled version of the
same:
ALTER ASSEMBLY AssemblyName
[AUTHORIZATION LoginName]
FROM StringPathToAssemblyDll | BinaryDataValue
[PERMISSION_SET = (SAFE | EXTERNAL_ACCESS | UNSAFE) ]
You can also use ALTER ASSEMBLY
to upload your C# class files so that when you’re debugging any
exceptions you get source code line numbers in the stack dump
(seamlessly reported by SQL Server’s built-in error reporting
mechanism). Here’s an example:
ALTER ASSEMBLY AssemblyName ADD FILE FROM FilePath
Tip
The
use of any managed user-defined types in your database prevents an assembly from being dropped or altered
until any type already in use is dropped (because it is a dependency).
After you load your assembly to SQL Server, you can execute the following DDL to add your managed stored procedure to AdventureWorks2008:
CREATE PROCEDURE [dbo].[clr_GetProductManuals]
WITH EXECUTE AS CALLER
AS
EXTERNAL NAME
[ProjectName].[StoredProcedures].[GetProductManuals]
The new WITH EXECUTE AS CALLER clause tells SQL Server that the permissions for executing the procedure should be those of its caller. (See the “EXECUTE AS” Books Online topic for more information.)
The new EXTERNAL NAME
keywords tell SQL Server that the routines or types being created
belong to a specific class in a loaded assembly. The dot-notation for
the string parameter of EXTERNAL NAME is as follows:
AssemblyName.ClassName.RoutineOrTypeName
You’ll see this parameter again in the DDL of the other managed objects we’ll soon create.
To view the objects created
during script execution or VS deployment, open the Object Explorer in
SQL Server Management Studio (SSMS), expand the AdventureWorks2008 database node, and then expand the Programmability node. There you can find the Assemblies
node. (The managed objects in the assembly are kept in their respective
folders.) If you right-click an assembly and view its properties,
another window appears where you can view or change the assembly’s
permissions.
Debugging Managed Code
Now open a new query window in SSMS and test your new managed stored procedure, just as you would any other. Try the following:
EXEC clr_GetProductManuals
Go
ProductModelId Manual
-----------------------------------------------------------------------------
7 <root xmlns="http://schemas.microsoft.com/sqlserver/2004/07/...
10 <root xmlns="http://schemas.microsoft.com/sqlserver/2004/07/...
...
(10 rows(s) affected.)
Let’s try debugging with VS. By default, every SQL Server project is created with a Test Scripts folder and a file called Test.sql. Test.sql (or any other .sql
file in a SQL Server project) is kind of like a hybrid of a code file
and a query window. In this file you can execute a batch of arbitrary
database commands, and you can also set breakpoints on each code line.
The output of the commands appears in the Database Output section of
VS’s Output window.
Enter the same T-SQL in Test.sql
that you just did in SSMS. Then set a breakpoint on any line. Press F5
or click the Run button, and you can now execute and step through your
managed stored procedure in a single environment.
You may first need to
acknowledge a dialog window that asks whether it’s okay to enable SQLCLR
debugging on the server. Answer in the affirmative, unless you’re in a
nontesting environment. (One caveat: As with remote debugging in
general, this setup may not work in all environments.)